package net.qiqbframework.modelling.domain;


import net.qiqbframework.common.Assert;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import static net.qiqbframework.common.utils.ReflectionUtil.ensureAccessible;
import static java.lang.String.format;

/**
 * Aggregate factory that uses a convention to create instances of aggregates. The type must declare a no-arg
 * constructor accepting.
 * <p>
 * If the constructor is not accessible (not public), and the JVM's security setting allow it, the
 * GenericAggregateFactory will try to make it accessible. If that doesn't succeed, an exception is thrown.
 *
 * @param <T> The type of aggregate this factory creates
 * 
 */
public class GenericAggregateFactory<T> extends AbstractAggregateFactory<T> {

    private final Map<Class<?>, Constructor<?>> constructors = new HashMap<>();

    /**
     * Initialize the AggregateFactory for creating instances of the given {@code aggregateModel}.
     *
     * @param aggregateModel the model of aggregate this factory creates instances of
     */
    public GenericAggregateFactory(AggregateModel<T> aggregateModel) {
        super(aggregateModel);
        aggregateModel.types()
                .filter(type -> !Modifier.isInterface(type.getModifiers()))
                .filter(type -> !Modifier.isAbstract(type.getModifiers()))
                .forEach(type -> {
                    try {
                        Constructor<?> constructor = ensureAccessible(type.getDeclaredConstructor());
                        constructors.put(type, constructor);
                    } catch (NoSuchMethodException e) {
                        throw new IncompatibleAggregateException(format(
                                "The aggregate [%s] doesn't provide a no-arg constructor.", type.getName()));
                    }
                });
    }

    /**
     * {@inheritDoc}
     * <p>
     *
     * @throws IncompatibleAggregateException if the aggregate constructor throws an exception, or if the JVM security
     *                                        settings prevent the GenericAggregateFactory from calling the
     *                                        constructor.
     */
    @SuppressWarnings({"unchecked"})
    @Override
    protected T doCreateAggregate(String aggregateIdentifier) {

        return newInstance(getAggregateType());
    }

    @SuppressWarnings("unchecked")
    private T newInstance(Class<?> type) {
        try {
            return (T) type.newInstance();
        } catch (InstantiationException e) {
            throw new IncompatibleAggregateException(format(
                    "The aggregate [%s] does not have a suitable no-arg constructor.",
                    type.getSimpleName()), e);
        } catch (IllegalAccessException e) {
            throw new IncompatibleAggregateException(format(
                    "The aggregate no-arg constructor of the aggregate [%s] is not accessible. Please ensure that "
                            + "the constructor is public or that the Security Manager allows access through "
                            + "reflection.", type.getSimpleName()), e);
        } 
    }
}
